/* 
 -----------------------------------------------------------------------------
                   Cogaen - Component-based Game Engine (v3)
 -----------------------------------------------------------------------------
 This software is developed by the Cogaen Development Team. Please have a 
 look at our project home page for further details: http://www.cogaen.org
    
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Copyright (c) 2010 Roman Divotkey

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*/

package org.cogaen.java2d;

import java.awt.Canvas;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.DisplayMode;
import java.awt.Frame;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.image.BufferStrategy;


/**
 * This screen class can be used to render output in full-screen or 
 * windowed mode. Screen mode can be changed at runtime.
 * 
 * <p>
 * Use one of the static methods {@code createWindowed} or 
 * {@code createFullScreen} to create a new instance of {@code ComboScreen}.
 * </p>
 */
public final class ComboScreen extends Screen implements HierarchyListener, WindowListener {

	/** Default number of buffers used for multi buffering */
	private static final int DEFAULT_NUM_BUFFERS = 2;
	
	/** Window used for full-screen mode. */
	private Frame frame;
	
	/** Display mode used for full-screen mode. */
	private DisplayMode displayMode;
	
	private GraphicsDevice device;
	
	/** Canvas used for windowed mode. */
	private Canvas canvas;
	
	/** Reference to BufferStrategy, used to return graphics context. */
	private BufferStrategy bufferStrategy;
	
	/** Indicates if this screen is already initialized. */
	private boolean initialized;
	
	/** Indicates if this screen is running in windowed or in full-screen mode. */
	private boolean fullScreen;

	private boolean invisibleCursor = false;

	/** Indicates if {@code System.exit(0)} should be called on window closing. */
	private boolean exitOnClose = true;

	/** Used to hide mouse cursor. */
	private static final Cursor INVISIBLE_CURSOR = createInvisibleCursor();
    
    /** Default mouse cursor. */
	private static final Cursor DEFAULT_CURSOR = new Cursor(Cursor.DEFAULT_CURSOR);
	
	/**
	 * Create a new instance in windowed mode.
	 * 
	 * @param width specifies the width of the screen in pixels.
	 * @param height specifies the height of the screen in pixels.
	 * @return new instance of {@code ComboScreen}.
	 */
	public static ComboScreen createWindowed(int width, int height) {
		ComboScreen screen = new ComboScreen(width, height);
		screen.initWindowed();
		return screen;
	}

	/**
	 * Creates a new instance in full-screen mode.
	 * 
	 * @param width specifies the width of the screen in pixels.
	 * @param height specifies the height of the screen in pixels.
	 * @return new instance of {@code ComboScreen}.
	 * @throws ScreenException in case of any problem switching to full-screen mode.
	 */
	public static ComboScreen createFullScreen(int width, int height) throws ScreenException {
		ComboScreen screen = new ComboScreen(width, height);
		screen.initFullscreen();
		return screen;
	}

	/**
	 * Creates a new instance in full-screen mode using the current screen
	 * width and height.
	 * 
	 * @return new instance of {@code ComboScreen}.
	 * @throws ScreenException in case of any problem switching to full-screen mode.
	 */
	public static ComboScreen createFullScreen() throws ScreenException {
		GraphicsDevice device = defaultGraphicsDevice();
		DisplayMode mode = device.getDisplayMode();
		ComboScreen screen = new ComboScreen(mode.getWidth(), mode.getHeight());
		screen.initFullscreen();
		return screen;
	}
		
	/**
	 * Creates a new instance of ComboScreen. The newly create screen object
	 * must be initialized with {@code initWindowed()}. or 
	 * {@code initFullScreen()}.
	 * 
	 * @param width specifies the width of the screen in pixels.
	 * @param height specifies the height of the screen in pixels.
	 */
	private ComboScreen(int width, int height) {
		super(width, height);
		this.frame = new Frame();
		this.frame.setResizable(false);
		this.frame.addWindowListener(this);
		this.device = defaultGraphicsDevice();
		this.displayMode = createDisplayMode(width, height);
	}
	
	private DisplayMode createDisplayMode(int width, int height) {
		DisplayMode currentMode = this.device.getDisplayMode();

		return new DisplayMode(width, height, 
				currentMode.getBitDepth(), 
				currentMode.getRefreshRate());
	}

	/**
	 * Specified is the behavior when the user attempts to close the window.
	 * 
	 * @param value {@code true} will close the application by calling
	 * {@code System.exit(0)}, {@code false} will do nothing.
	 */
	public void setExitOnClosing(boolean value) {
		this.exitOnClose = value;
	}
	
	/**
	 * Returns the specified behavior when the user attempts to close the window.
	 * 
	 * @return {@code true} if {@code System.exit(0)} will be called, 
	 * {@code false} otherwise.
	 */
	public boolean isExitOnClosing() {
		return this.exitOnClose;
	}
	
	/**
	 * Sets the window title of this screen.
	 * 
	 * <p>The title is not visible while in full-screen mode.</p>
	 * @param title the title of this screen.
	 */
	public void setTitle(String title) {
		this.frame.setTitle(title);
	}
	
	/**
	 * Retrieves default graphics device of local {@code GraphicsEnvironment}.
	 * 
	 * @return GraphicsDevice from the local graphics environment.
	 */
	private static GraphicsDevice defaultGraphicsDevice() {
		GraphicsEnvironment ge = 
			GraphicsEnvironment.getLocalGraphicsEnvironment();

		return ge.getDefaultScreenDevice();
	}
	
	/**
	 * Changes the screen mode of this instance.
	 * 
	 * @param value {@code true} if this screen should run in full-screen mode 
	 * or {@code false} for windowed mode.
	 * @throws ScreenException in case of any problem while changing screen mode.
	 */
	public void setFullscreen(boolean value) throws ScreenException {
		if (value) {
			if (this.fullScreen) {
				return;
			}
			this.frame.remove(this.canvas);
			this.frame.dispose();
			initFullscreen();
			if (this.invisibleCursor) {
				this.frame.setCursor(INVISIBLE_CURSOR);
			}
		} else {
			if (!this.fullScreen) {
				return;
			}
			this.device.setFullScreenWindow(null);
			this.frame.dispose();
			initWindowed();
			this.frame.setCursor(DEFAULT_CURSOR);
		}
	}	
	
	private void initFullscreen() throws ScreenException {
		this.frame.setUndecorated(true);
		this.frame.setIgnoreRepaint(true);
		this.device.setFullScreenWindow(this.frame);
		try {
			this.device.setDisplayMode(this.displayMode);
		} catch (IllegalArgumentException e){
			this.device.setFullScreenWindow(null);
			this.frame.dispose();
			initWindowed();
			throw new ScreenException("display mode not supported", e);
		} catch (Exception e) {
			this.device.setFullScreenWindow(null);
			this.frame.dispose();
			initWindowed();
			throw new ScreenException("unable to switch to full-screen mode", e);			
		}
		
		this.frame.createBufferStrategy(DEFAULT_NUM_BUFFERS);
		this.bufferStrategy = this.frame.getBufferStrategy();
		setInitialized(true);
		this.fullScreen = true;
	}
	
	private void initWindowed() {
		if (this.canvas == null) {
			this.canvas = new Canvas();
			this.canvas.setIgnoreRepaint(true);
			this.canvas.setFocusable(false);
			this.canvas.addHierarchyListener(this);
		}
		
		setInitialized(false);
		this.bufferStrategy = null;
		this.canvas.setSize(getWidth(), getHeight());
		this.canvas.setPreferredSize(new Dimension(getWidth(), getHeight()));
		this.frame.setUndecorated(false);
		this.frame.setIgnoreRepaint(false);
		this.frame.add(this.canvas);
		this.frame.pack();
		this.frame.setVisible(true);
		this.fullScreen = false;
		
		waitUntilReady();
	}
	
	private void initCanvas() {
		if (this.bufferStrategy != null) {
			return;
		}
		this.canvas.createBufferStrategy(DEFAULT_NUM_BUFFERS);
		this.bufferStrategy = this.canvas.getBufferStrategy();
		setInitialized(true);
	}
	
	private synchronized void setInitialized(boolean value) {
		this.initialized = value;
		notify();
	}
	
	private synchronized boolean isInitialized() {
		return this.initialized;
	}

	/** 
	 * Created an invisible cursor used to hide the standard mouse cursor.
	 * 
	 * @return Cursor object that shows nothing.
	 */
    private static Cursor createInvisibleCursor() {
    	return Toolkit.getDefaultToolkit().createCustomCursor(
    			Toolkit.getDefaultToolkit().getImage(""), new Point(0, 0), "invisible");
    }
	
	@Override
	public Component getComponent() {
		return this.frame;
	}

    /**
     * Waits till this screen is initialized and ready to render output.
     * <p>Any attempt to render output before this screen is initialize might 
     * cause a runtime exception.</p>
     */
	private synchronized void waitUntilReady() {
		while (!isInitialized()) {
			try {
				wait();
			} catch (InterruptedException e) {
				Thread.currentThread().interrupt();
			}
		}
	}

	@Override
	public Graphics2D getGraphicsContext() {
		return (Graphics2D) this.bufferStrategy.getDrawGraphics();
	}

	@Override
	public void update() {
		bufferStrategy.show();

		// Sync the display on some systems.
		// (on Linux, this fixes event queue problems)
		Toolkit.getDefaultToolkit().sync();
	}

	@Override
	public void hideMouseCursor() {
		this.invisibleCursor  = true;
		if (isFullscreen()) {
			frame.setCursor(INVISIBLE_CURSOR);
		}
	}

	@Override
	public void showMouseCursor() {
		this.invisibleCursor = false;
		if (isFullscreen()) {
	    	frame.setCursor(DEFAULT_CURSOR);			
		}
	}

	@Override
	public GraphicsConfiguration getGraphicsConfiguration() {
		return this.frame.getGraphicsConfiguration();
	}
	
	@Override
	public boolean isFullscreen() {
		return this.fullScreen;
	}

	@Override
	public void hierarchyChanged(HierarchyEvent event) {
		long query = event.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED;
		if (query != 0 && this.canvas.isShowing()) {
			initCanvas();
		}
	}

	@Override
	public void windowOpened(WindowEvent e) {
		// intentionally left empty
	}

	@Override
	public void windowClosing(WindowEvent e) {
		if (this.exitOnClose) {
			System.exit(0);
		}
	}

	@Override
	public void windowClosed(WindowEvent e) {
		// intentionally left empty
	}

	@Override
	public void windowIconified(WindowEvent e) {
		// intentionally left empty
	}

	@Override
	public void windowDeiconified(WindowEvent e) {
		// intentionally left empty
	}

	@Override
	public void windowActivated(WindowEvent e) {
		// intentionally left empty
	}

	@Override
	public void windowDeactivated(WindowEvent e) {
		// intentionally left empty
	}
}
